package com.halden.TRPG.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.halden.TRPG.common.CodeEnum;
import com.halden.TRPG.common.MyException;
import com.halden.TRPG.entity.MultipartFileParam;
import com.halden.TRPG.entity.RoomFileEntity;
import com.halden.TRPG.mapper.RoomFileMapper;
import com.halden.TRPG.service.RoomFileService;
import io.micrometer.core.instrument.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.annotations.Param;
import org.apache.commons.io.FileUtils;
import org.bson.types.Code;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.FileInputStream;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.RandomAccessFile;

@Service
@Transactional(rollbackFor = MyException.class)
@Slf4j
public class RoomFileServiceImpl extends ServiceImpl<RoomFileMapper, RoomFileEntity> implements RoomFileService {

    @Autowired
    private String staticPath;

    @Value(("${on-path}"))
    private String onPath;

    @Override
    public CodeEnum chunkUploadByMappedByteBuffer(MultipartFileParam param, Long rid) {
        log.info("uploading file");
        String filePath = staticPath + "room/" + rid + "/file";
        if(param.getTaskId() == null || "".equals(param.getTaskId())){
            param.setTaskId(UUID.randomUUID().toString());
        }
        try {
            /**
             *
             * 1：创建临时文件，和源文件一个路径
             * 2：如果文件路径不存在重新创建
             */
            String fileName = param.getFile().getOriginalFilename();
            String tempFileName = param.getTaskId() + fileName.substring(fileName.lastIndexOf(".")) + "_tmp";
            File fileDir = new File(filePath);
            if (!fileDir.exists()) {
                fileDir.mkdirs();
            }
            File tempFile = new File(filePath, tempFileName);
            //第一步
            RandomAccessFile raf = new RandomAccessFile(tempFile, "rw");
            //第二步 创建fileChannel
            FileChannel fileChannel = raf.getChannel();
            //第三步 计算偏移量
            long position = (param.getChunkNumber() - 1) * param.getChunkSize();
            //第四步 读取文件分片的内容
            byte[] fileData = param.getFile().getBytes();
            //第五步
            long end = position + fileData.length - 1;
            //定位偏移量
            fileChannel.position(position);
            //写入临时文件
            fileChannel.write(ByteBuffer.wrap(fileData));
            //使用 fileChannel.map的方式速度更快，但是容易产生IO操作，无建议使用
//        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE,position,fileData.length);
//        //第六步
//        mappedByteBuffer.put(fileData);
            //第七步
//        freedMappedByteBuffer(mappedByteBuffer);
//        Method method = FileChannelImpl.class.getDeclaredMethod("unmap", MappedByteBuffer.class);
//        method.setAccessible(true);
//        method.invoke(FileChannelImpl.class, mappedByteBuffer);
            fileChannel.force(true);
            fileChannel.close();
            raf.close();
            //第八步
            boolean isComplete = checkUploadStatus(param, fileName, filePath);
            if (isComplete) {
                //校验MD5文件是否一致,然后重命名文件
                FileInputStream md5CheckStream = new FileInputStream(tempFile.getPath());
                String md5 = DigestUtils.md5DigestAsHex(md5CheckStream);
                md5CheckStream.close();
                renameFile(tempFile, fileName);
                if (StringUtils.isNotBlank(md5) && !md5.equals(param.getIdentifier())) {
                    //不是同一文件抛出异常
                    return CodeEnum.NOT_SAME_FILE_EXCEPTION;
                }
                boolean insertEntity = insertEntity(rid, fileName);
            }
            return CodeEnum.SUCCESS.setData(param.getTaskId());
        } catch (Exception e){
            e.printStackTrace();
            throw new MyException(CodeEnum.OPTION_FAIL_EXCEPTION);
        }
    }

    /**
     * 文件重命名
     * @param toBeRenamed   将要修改名字的文件
     * @param toFileNewName 新的名字
     * @return
     */
    public static void renameFile(File toBeRenamed, String toFileNewName) {
        //检查要重命名的文件是否存在，是否是文件
        if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {
            System.out.println("文件不存在");
            return;
        }
        String p = toBeRenamed.getParent();
        File newFile = new File(p + File.separatorChar + toFileNewName);
        //修改文件名
        boolean rename = toBeRenamed.renameTo(newFile);
        log.info("rename "+toBeRenamed.getPath()+" to "+newFile.getPath() + ", result: "+rename);
    }

    /**
     * 检查文件上传进度
     * @return
     */
    public boolean checkUploadStatus(MultipartFileParam param,String fileName,String filePath) {
        try {
            File confFile = new File(filePath, fileName + ".conf");
            RandomAccessFile confAccessFile = new RandomAccessFile(confFile, "rw");
            //设置文件长度
            confAccessFile.setLength(param.getTotalChunks());
            //设置起始偏移量
            confAccessFile.seek(param.getChunkNumber() - 1);
            //将指定的一个字节写入文件中 127，
            confAccessFile.write(Byte.MAX_VALUE);
            byte[] completeStatusList = FileUtils.readFileToByteArray(confFile);
            confAccessFile.close();//不关闭会造成无法占用
            //创建conf文件文件长度为总分片数，每上传一个分块即向conf文件中写入一个127，那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127
            for (int i = 0; i < completeStatusList.length; i++) {
                if (completeStatusList[i] != Byte.MAX_VALUE) {
                    return false;
                }
            }
            //如果全部文件上传完成，删除conf文件
            confFile.delete();
            return true;
        } catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }

    @Override
    public CodeEnum getFileList(Long rid) {
        List<RoomFileEntity> fileEntityList = baseMapper.selectList(new QueryWrapper<RoomFileEntity>().eq("rid", rid));
        if (fileEntityList == null || fileEntityList.size() == 0){
            return CodeEnum.SUCCESS.setData(new ArrayList<RoomFileEntity>());
        }
        return CodeEnum.SUCCESS.setData(fileEntityList);
    }

    @Override
    public boolean deleteRoomFile(Long rid) {
        int delete = baseMapper.delete(new QueryWrapper<RoomFileEntity>().eq("rid", rid));
        return delete >= 0;
    }

    /**
     * 在MappedByteBuffer释放后再对它进行读操作的话就会引发jvm crash，在并发情况下很容易发生
     * 正在释放时另一个线程正开始读取，于是crash就发生了。所以为了系统稳定性释放前一般需要检 查是否还有线程在读或写
     * @param mappedByteBuffer
     */
    public static void freedMappedByteBuffer(final MappedByteBuffer mappedByteBuffer) {
        try {
            if (mappedByteBuffer == null) {
                return;
            }
            mappedByteBuffer.force();
            AccessController.doPrivileged(new PrivilegedAction<Object>() {
                @Override
                public Object run() {
                    try {
                        Method getCleanerMethod = mappedByteBuffer.getClass().getMethod("cleaner", new Class[0]);
                        //可以访问private的权限
                        getCleanerMethod.setAccessible(true);
                        //在具有指定参数的 方法对象上调用此 方法对象表示的底层方法
                        sun.misc.Cleaner cleaner = (sun.misc.Cleaner) getCleanerMethod.invoke(mappedByteBuffer,
                                new Object[0]);
                        cleaner.clean();
                    } catch (Exception e) {
                        e.printStackTrace();
                        System.out.println("清理缓存出错!!!"+e.getMessage());
                    }
                    System.out.println("缓存清理完毕!!!");
                    return null;
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public boolean insertEntity(Long rid, String fileName){
        String url = onPath+"room/"+rid+"/file/"+fileName;
        RoomFileEntity entity = new RoomFileEntity();
        entity.setRid(rid);
        entity.setFilename(fileName);
        entity.setUrl(url);
        boolean save = save(entity);
        return save;
    }


}
